import { expect } from 'chai';
import {
  boolean,
  decimal,
  eachLike,
  hexadecimal,
  integer,
  ipv4Address,
  ipv6Address,
  ISO8601_DATE_FORMAT,
  iso8601Date,
  iso8601DateTime,
  iso8601DateTimeWithMillis,
  iso8601Time,
  rfc1123Timestamp,
  somethingLike,
  string,
  term,
  email,
  uuid,
  validateExample,
  extractPayload,
  isMatcher,
  like,
  AnyTemplate,
  InterfaceToTemplate,
} from './matchers';

interface ExampleInterface {
  someString: string;
  someArray: Array<string>;
  someNumber: number;
  someObject: {
    foo: string;
    bar: string;
  };
}

type ExampleType = {
  someString: string;
  someArray: Array<string>;
  someNumber: number;
  someObject: {
    foo: string;
    bar: string;
  };
};

describe('Matcher', () => {
  describe('can compile the types', () => {
    describe('with interfaces', () => {
      it('compiles with nested examples from issue 1054', () => {
        interface Foo {
          a: string;
        }

        const f: Foo = { a: 'working example' };

        like(f);
        like({ ...f });
        like(Object.freeze(f));

        interface Room {
          id: string;
          foo: Foo;
        }

        const r: Room = { id: 'some guid', foo: { a: 'example' } };

        like(r);
        like({ ...r });
        like(Object.freeze(f));
      });

      it('compiles when InterfaceToTemplate is used', () => {
        const template: InterfaceToTemplate<ExampleInterface> = {
          someArray: ['one', 'two'],
          someNumber: 1,
          someString: "it's a string",
          someObject: {
            foo: 'some string',
            bar: 'some other string',
          },
        };

        const unused: AnyTemplate = like(template);
      });
    });

    describe('with types', () => {
      it('compiles', () => {
        const template: ExampleType = {
          someArray: ['one', 'two'],
          someNumber: 1,
          someString: "it's a string",
          someObject: {
            foo: 'some string',
            bar: 'some other string',
          },
        };

        const unused: AnyTemplate = like(template);
      });
    });

    it('compiles nested likes', () => {
      const unused: AnyTemplate = like({
        someArray: ['one', 'two'],
        someNumber: like(1),
        someString: "it's a string",
        someObject: like({
          foo: like('some string'),
          bar: 'some other string',
        }),
      });
    });
  });

  describe('#validateExample', () => {
    describe('when given a valid regex', () => {
      describe('and a matching example', () => {
        it('returns true', () => {
          expect(validateExample('2010-01-01', ISO8601_DATE_FORMAT)).to.eql(
            true
          );
        });
      });

      describe('and a failing example', () => {
        it('returns false', () => {
          expect(validateExample('not a date', ISO8601_DATE_FORMAT)).to.eql(
            false
          );
        });
      });
    });

    describe('when given an invalid regex', () => {
      it('returns an error', () => {
        expect(() => {
          validateExample('', 'abc(');
        }).to.throw(Error);
      });
    });
  });

  describe('#term', () => {
    describe('when given a valid regular expression and example', () => {
      it('returns a serialized Ruby object', () => {
        const expected = {
          value: 'myawesomeword',
          regex: '\\w+',
          'pact:matcher:type': 'regex',
        };

        const match = term({
          generate: 'myawesomeword',
          matcher: '\\w+',
        });

        expect(JSON.stringify(match)).to.deep.include(JSON.stringify(expected));
      });
    });

    describe('when not provided with a valid expression', () => {
      const createTheTerm = (badArg: any) => () => {
        term(badArg);
      };

      describe('when no term is provided', () => {
        it('throws an Error', () => {
          expect(createTheTerm.call({})).to.throw(Error);
        });
      });

      describe('when an invalid term is provided', () => {
        it('throws an Error', () => {
          expect(createTheTerm({})).to.throw(Error);
          expect(createTheTerm('')).to.throw(Error);
          expect(createTheTerm({ value: 'foo' })).to.throw(Error);
          expect(createTheTerm({ matcher: '\\w+' })).to.throw(Error);
        });
      });
    });

    describe("when given an example that doesn't match the regular expression", () => {
      it('fails with an error', () => {
        expect(() => {
          term({
            generate: 'abc',
            matcher: ISO8601_DATE_FORMAT,
          });
        }).to.throw(Error);
      });
    });
  });

  describe('#somethingLike', () => {
    describe('when provided a value', () => {
      it('returns a serialized Ruby object', () => {
        const expected = {
          value: 'myspecialvalue',
          'pact:matcher:type': 'type',
        };

        const match = somethingLike('myspecialvalue');
        expect(JSON.stringify(match)).to.deep.include(JSON.stringify(expected));
      });
    });

    describe('when not provided with a valid value', () => {
      const createTheValue = (badArg: any) => () => {
        somethingLike(badArg);
      };

      describe('when no value is provided', () => {
        it('`throws an Error', () => {
          expect(createTheValue.call({})).to.throw(Error);
        });
      });

      describe('when an invalid value is provided', () => {
        it('throws an Error', () => {
          expect(createTheValue(undefined)).to.throw(Error);
          expect(createTheValue(() => {})).to.throw(Error);
        });
      });
    });
  });

  describe('#eachLike', () => {
    describe('when content is null', () => {
      it('provides null as contents', () => {
        const expected = {
          value: [null],
          'pact:matcher:type': 'type',
          min: 1,
        };

        const match = eachLike(null, { min: 1 });
        expect(JSON.stringify(match)).to.deep.include(JSON.stringify(expected));
      });
    });

    describe('when an object is provided', () => {
      it('provides the object as contents', () => {
        const expected = {
          value: [{ a: 1 }],
          'pact:matcher:type': 'type',
          min: 1,
        };

        const match = eachLike({ a: 1 }, { min: 1 });
        expect(JSON.stringify(match)).to.deep.include(JSON.stringify(expected));
      });
    });

    describe('when object.min is invalid', () => {
      it('throws an Error message', () => {
        expect(() => {
          eachLike({ a: 1 }, { min: 0 });
        }).to.throw(Error);
      });
    });

    describe('when an array is provided', () => {
      it('provides the array as contents', () => {
        const expected = {
          value: [[1, 2, 3]],
          'pact:matcher:type': 'type',
          min: 1,
        };

        const match = eachLike([1, 2, 3], { min: 1 });
        expect(JSON.stringify(match)).to.deep.include(JSON.stringify(expected));
      });
    });

    describe('when a value is provided', () => {
      it('adds the value in contents', () => {
        const expected = {
          value: ['test'],
          'pact:matcher:type': 'type',
          min: 1,
        };

        const match = eachLike('test', { min: 1 });
        expect(JSON.stringify(match)).to.deep.include(JSON.stringify(expected));
      });
    });

    describe('when the content has Pact.Macters', () => {
      describe('of type somethingLike', () => {
        it('nests somethingLike correctly', () => {
          const expected = {
            value: [
              {
                id: {
                  value: 10,
                  'pact:matcher:type': 'type',
                },
              },
            ],
            'pact:matcher:type': 'type',
            min: 1,
          };

          const match = eachLike({ id: somethingLike(10) }, { min: 1 });
          expect(JSON.stringify(match)).to.deep.include(
            JSON.stringify(expected)
          );
        });
      });

      describe('of type term', () => {
        it('nests term correctly', () => {
          const expected = {
            value: [
              {
                colour: {
                  value: 'red',
                  regex: 'red|green',
                  'pact:matcher:type': 'regex',
                },
              },
            ],
            'pact:matcher:type': 'type',
            min: 1,
          };

          const match = eachLike(
            {
              colour: term({
                generate: 'red',
                matcher: 'red|green',
              }),
            },
            { min: 1 }
          );

          //

          expect(JSON.stringify(match)).to.deep.include(
            JSON.stringify(expected)
          );
        });
      });

      describe('of type eachLike', () => {
        it('nests eachlike in contents', () => {
          const expected = {
            value: [
              {
                value: ['blue'],
                'pact:matcher:type': 'type',
                min: 1,
              },
            ],
            'pact:matcher:type': 'type',
            min: 1,
          };

          const match = eachLike(eachLike('blue', { min: 1 }), { min: 1 });
          expect(JSON.stringify(match)).to.deep.include(
            JSON.stringify(expected)
          );
        });
      });

      describe('complex object with multiple Pact.Matchers', () => {
        it('nests objects correctly', () => {
          const expected = {
            value: [
              {
                value: [
                  {
                    colour: {
                      value: 'red',
                      'pact:matcher:type': 'regex',
                      regex: 'red|green|blue',
                    },
                    size: {
                      value: 10,
                      'pact:matcher:type': 'type',
                    },
                    tag: {
                      value: [
                        [
                          {
                            value: 'jumper',
                            'pact:matcher:type': 'type',
                          },
                          {
                            value: 'shirt',
                            'pact:matcher:type': 'type',
                          },
                        ],
                        [
                          {
                            value: 'jumper',
                            'pact:matcher:type': 'type',
                          },
                          {
                            value: 'shirt',
                            'pact:matcher:type': 'type',
                          },
                        ],
                      ],
                      'pact:matcher:type': 'type',
                      min: 2,
                    },
                  },
                ],
                'pact:matcher:type': 'type',
                min: 1,
              },
            ],
            'pact:matcher:type': 'type',
            min: 1,
          };

          const match = eachLike(
            eachLike(
              {
                colour: term({ generate: 'red', matcher: 'red|green|blue' }),
                size: somethingLike(10),
                tag: eachLike(
                  [somethingLike('jumper'), somethingLike('shirt')],
                  { min: 2 }
                ),
              },
              { min: 1 }
            ),
            { min: 1 }
          );

          expect(JSON.parse(JSON.stringify(match))).to.deep.include(
            JSON.parse(JSON.stringify(expected))
          );
        });
      });
    });

    describe('When no options.min is not provided', () => {
      it('defaults to a min of 1', () => {
        const expected = {
          value: [{ a: 1 }],
          'pact:matcher:type': 'type',
          min: 1,
        };

        const match = eachLike({ a: 1 });
        expect(JSON.stringify(match)).to.deep.include(JSON.stringify(expected));
      });
    });

    describe('When a options.min is provided', () => {
      it('provides the object as contents', () => {
        const expected = {
          value: [{ a: 1 }, { a: 1 }, { a: 1 }],
          'pact:matcher:type': 'type',
          min: 3,
        };

        const match = eachLike({ a: 1 }, { min: 3 });
        expect(JSON.stringify(match)).to.deep.include(JSON.stringify(expected));
      });
    });
  });

  describe('#email', () => {
    describe('when given a valid Email address', () => {
      it('creates a valid matcher', () => {
        expect(email('hello@world.com')).to.be.an('object');
        expect(email('hello@world.com.au')).to.be.an('object');
        expect(email('hello@a.co')).to.be.an('object');
        expect(email()).to.be.an('object');
      });
    });

    describe('when given an invalid Email address', () => {
      it('returns an error', () => {
        expect(() => {
          email('hello.world.c');
        }).to.throw(Error);
      });
    });
  });

  describe('#uuid', () => {
    describe('when given a valid UUID', () => {
      it('creates a valid matcher', () => {
        expect(uuid('ce118b6e-d8e1-11e7-9296-cec278b6b50a')).to.be.an('object');
        expect(uuid()).to.be.an('object');
      });
    });

    describe('when given an invalid UUID', () => {
      it('returns an error', () => {
        expect(() => {
          uuid('abc');
        }).to.throw(Error);
      });
    });
  });

  describe('#ipv4Address', () => {
    describe('when given a valid ipv4Address', () => {
      it('creates a valid matcher', () => {
        expect(ipv4Address('127.0.0.1')).to.be.an('object');
        expect(ipv4Address()).to.be.an('object');
      });
    });

    describe('when given an invalid ipv4Address', () => {
      it('returns an error', () => {
        expect(() => {
          ipv4Address('abc');
        }).to.throw(Error);
      });
    });
  });

  describe('#ipv6Address', () => {
    describe('when given a valid ipv6Address', () => {
      it('creates a valid matcher', () => {
        expect(ipv6Address('::1')).to.be.an('object');
        expect(ipv6Address('2001:0db8:85a3:0000:0000:8a2e:0370:7334')).to.be.an(
          'object'
        );
        expect(ipv6Address()).to.be.an('object');
      });
    });

    describe('when given an invalid ipv6Address', () => {
      it('returns an error', () => {
        expect(() => {
          ipv6Address('abc');
        }).to.throw(Error);
      });
    });
  });

  describe('#hexadecimal', () => {
    describe('when given a valid hexadecimal', () => {
      it('creates a valid matcher', () => {
        expect(hexadecimal('6F')).to.be.an('object');
        expect(hexadecimal()).to.be.an('object');
      });
    });

    describe('when given an invalid hexadecimal', () => {
      it('returns an error', () => {
        expect(() => {
          hexadecimal('x1');
        }).to.throw(Error);
      });
    });
  });

  describe('#boolean', () => {
    describe('when used it should create a JSON object', () => {
      it('creates a valid matcher', () => {
        expect(boolean()).to.be.an('object');
        expect(boolean().value).to.equal(true);
      });

      it('sets value=false', () => {
        expect(boolean(false)).to.be.an('object');
        expect(boolean(false).value).to.equal(false);
      });

      it('sets value=true', () => {
        expect(boolean(true)).to.be.an('object');
        expect(boolean(true).value).to.equal(true);
      });
    });
  });

  describe('#string', () => {
    describe('when given a valid string', () => {
      it('creates a valid matcher', () => {
        expect(string('test')).to.be.an('object');
        expect(string()).to.be.an('object');
        expect(string('test').value).to.equal('test');
      });
    });
  });

  describe('#decimal', () => {
    describe('when given a valid decimal', () => {
      it('creates a valid matcher', () => {
        expect(decimal(10.1)).to.be.an('object');
        expect(decimal()).to.be.an('object');
        expect(decimal(0.0).value).to.equal(0.0);
      });
    });
  });

  describe('#integer', () => {
    describe('when given a valid integer', () => {
      it('creates a valid matcher', () => {
        expect(integer(10)).to.be.an('object');
        expect(integer()).to.be.an('object');
        expect(integer(0).value).to.equal(0);
      });
    });
  });

  describe('Date Matchers', () => {
    describe('#rfc1123Timestamp', () => {
      describe('when given a valid rfc1123Timestamp', () => {
        it('creates a valid matcher', () => {
          expect(rfc1123Timestamp('Mon, 31 Oct 2016 15:21:41 -0400')).to.be.an(
            'object'
          );
          expect(rfc1123Timestamp()).to.be.an('object');
        });
      });

      describe('when given an invalid rfc1123Timestamp', () => {
        it('returns an error', () => {
          expect(() => {
            rfc1123Timestamp('abc');
          }).to.throw(Error);
        });
      });
    });

    describe('#iso8601Time', () => {
      describe('when given a valid iso8601Time', () => {
        it('creates a valid matcher', () => {
          expect(iso8601Time('T22:44:30.652Z')).to.be.an('object');
          expect(iso8601Time()).to.be.an('object');
        });
      });

      describe('when given an invalid iso8601Time', () => {
        it('returns an error', () => {
          expect(() => {
            iso8601Time('abc');
          }).to.throw(Error);
        });
      });
    });

    describe('#iso8601Date', () => {
      describe('when given a valid iso8601Date', () => {
        it('creates a valid matcher', () => {
          expect(iso8601Date('2017-12-05')).to.be.an('object');
          expect(iso8601Date()).to.be.an('object');
        });
      });

      describe('when given an invalid iso8601Date', () => {
        it('returns an error', () => {
          expect(() => {
            iso8601Date('abc');
          }).to.throw(Error);
        });
      });
    });

    describe('#iso8601DateTime', () => {
      describe('when given a valid iso8601DateTime', () => {
        it('creates a valid matcher', () => {
          expect(iso8601DateTime('2015-08-06T16:53:10+01:00')).to.be.an(
            'object'
          );
          expect(iso8601DateTime()).to.be.an('object');
        });
      });

      describe('when given an invalid iso8601DateTime', () => {
        it('returns an error', () => {
          expect(() => {
            iso8601DateTime('abc');
          }).to.throw(Error);
        });
      });
    });

    describe('#iso8601DateTimeWithMillis', () => {
      describe('when given a valid iso8601DateTimeWithMillis', () => {
        it('creates a valid matcher', () => {
          expect(
            iso8601DateTimeWithMillis('2015-08-06T16:53:10.123+01:00')
          ).to.be.an('object');
          expect(
            iso8601DateTimeWithMillis('2015-08-06T16:53:10.537357Z')
          ).to.be.an('object');
          expect(iso8601DateTimeWithMillis('2020-12-10T09:01:29.06Z')).to.be.an(
            'object'
          );
          expect(iso8601DateTimeWithMillis('2020-12-10T09:01:29.1Z')).to.be.an(
            'object'
          );
          expect(iso8601DateTimeWithMillis()).to.be.an('object');
        });
      });

      describe('when given an invalid iso8601DateTimeWithMillis', () => {
        it('returns an error', () => {
          expect(() => {
            iso8601DateTimeWithMillis('abc');
          }).to.throw(Error);
        });
      });
    });

    describe('#extractPayload', () => {
      describe('when given an object with no matchers', () => {
        const object = {
          some: 'data',
          more: 'strings',
          an: ['array'],
          someObject: {
            withData: true,
            withNumber: 1,
          },
        };

        it('returns just that object', () => {
          expect(extractPayload(object)).to.deep.equal(object);
        });
      });

      describe('when given an object with null values', () => {
        const object = {
          some: 'data',
          more: null,
          an: [null],
          someObject: {
            withData: true,
            withNumber: 1,
            andNull: null,
          },
        };

        it('returns just that object', () => {
          expect(extractPayload(object)).to.deep.equal(object);
        });
      });

      describe('when given an object with some matchers', () => {
        const someMatchers = {
          some: somethingLike('data'),
          more: 'strings',
          an: ['array'],
          another: eachLike('this'),
          someObject: {
            withData: somethingLike(true),
            withTerm: term({ generate: 'this', matcher: 'this|that' }),
            withNumber: 1,
            withAnotherNumber: somethingLike(2),
          },
        };

        const expected = {
          some: 'data',
          more: 'strings',
          an: ['array'],
          another: ['this'],
          someObject: {
            withData: true,
            withTerm: 'this',
            withNumber: 1,
            withAnotherNumber: 2,
          },
        };

        it('returns without matching guff', () => {
          expect(extractPayload(someMatchers)).to.deep.equal(expected);
        });
      });

      describe('when given a simple matcher', () => {
        it('removes all matching guff', () => {
          const expected = 'myawesomeword';

          const matcher = term({
            generate: 'myawesomeword',
            matcher: '\\w+',
          });

          expect(isMatcher(matcher)).to.eq(true);
          expect(extractPayload(matcher)).to.eql(expected);
        });
      });

      describe('when given a complex nested object with matchers', () => {
        it('removes all matching guff', () => {
          const o = somethingLike({
            stringMatcher: {
              awesomeSetting: somethingLike('a string'),
            },
            anotherStringMatcher: {
              nestedSetting: {
                anotherStringMatcherSubSetting: somethingLike(true),
              },
              anotherSetting: term({ generate: 'this', matcher: 'this|that' }),
            },
            arrayMatcher: {
              lotsOfValueregex: eachLike('useful', { min: 3 }),
            },
            arrayOfMatcherregex: {
              lotsOfValueregex: eachLike(
                {
                  foo: 'bar',
                  baz: somethingLike('bat'),
                },
                { min: 3 }
              ),
            },
          });

          const expected = {
            stringMatcher: {
              awesomeSetting: 'a string',
            },
            anotherStringMatcher: {
              nestedSetting: {
                anotherStringMatcherSubSetting: true,
              },
              anotherSetting: 'this',
            },
            arrayMatcher: {
              lotsOfValueregex: ['useful', 'useful', 'useful'],
            },
            arrayOfMatcherregex: {
              lotsOfValueregex: [
                {
                  baz: 'bat',
                  foo: 'bar',
                },
                {
                  baz: 'bat',
                  foo: 'bar',
                },
                {
                  baz: 'bat',
                  foo: 'bar',
                },
              ],
            },
          };

          expect(extractPayload(o)).to.deep.equal(expected);
        });
      });
    });
  });
});
